1. Do czego w Expressie służy metoda res.send(arg)?
Wyjaśnienie
Tej metody używamy głównie w celu wyświetlenia komunikatów o błędzie.
Dotychczas główną technologią, której używaliśmy w naszych aplikacjach do komunikacji klient-serwer był AJAX. Pozwalał on na łatwe tworzenie requestów i odbieranie odpowiedzi, przy czym strona nie musiała się nawet przeładowywać. Mimo wielu zalet nie zawsze jest to rozwiązanie optymalne. Przekonaliśmy się o tym, chociażby w poprzednim module, gdzie podstrona do rezerwacji biletów miała problem z pokazywaniem aktualnych danych o liczbie zajętych miejsc. Rozwiązaniem tego oraz wielu innych problemów, może być technologia, którą poznasz w tym module, a mianowicie WebSocket.
AJAX bazuje na komunikacji bez stałego połączenia, która może być inicjowana tylko z jednej strony (klient wysyła request do serwera), natomiast WebSocket pozwala na stałą łączność oraz wysyłanie danych przez oba podmioty. Dzięki temu wymiana danych między hostami może być natychmiastowa. Takie rozwiązanie ma ogromną liczbę zastosowań.
Przykładami użycia mogą być:
Zacznijmy od przypomnienia, jak działa AJAX.
Gdy tylko zachodzi potrzeba komunikacji z serwerem, wysyłamy żądanie (request) i otrzymujemy odpowiedź (response). Co ważne – nie mamy stałego połączenia, a komunikujemy się tylko na chwilę, w celu otrzymania odpowiedzi, po czym tracimy kontakt aż do momentu, kiedy wyślemy kolejne żądanie. W niektórych aplikacjach robimy to tylko raz (np. pobierając startowe dane), a czasami częściej (np. cyklicznie odświeżamy listę).
W tym przypadku mamy aż trzech klientów łączących się co jakiś czas z serwerem za pomocą requestów AJAX-u. Nawet gdyby wysyłali je co kilka sekund, to i tak przez chwilę nie będą mieli kontaktu z serwerem. Ponadto zbyt częste żądania generują ogromne przeciążenie – wyobraź sobie, że w tym czasie serwer musi obsłużyć request od stu czy tysiąca klientów. Nie jest to komfortowa sytuacja.
Oczywiście taki sposób komunikacji niekoniecznie musi być wadą. Jeśli wykorzystujemy serwer sporadycznie, do aktualizacji czy pobierania jakichś danych, często będzie to dobra opcja. Zresztą, z doświadczenia wiesz już, że AJAX w większości przypadków radzi sobie świetnie.
Są jednak sytuacje, w których brak stałego połączenia będzie nam szczególnie doskwierał – zwłaszcza gdy często wymieniamy informacje z serwerem, albo nie wiemy, kiedy powinniśmy je zaktualizować.
Przypomnij sobie przykład witryny festiwalu muzycznego z poprzedniego modułu. Przez większość czasu "zajętość" miejsc w bazie wcale się nie zmienia. Możliwa jest sytuacja, że nawet przez kilka godzin żaden użytkownik nic nie zarezerwuje, a w tym czasie aplikacja (klient) i tak będzie uparcie "pytać" serwer, jaki jest aktualny stan.
Tutaj ujawnia się jeszcze inna wada AJAXu: serwer sam nie może rozpoczynać połączenia, więc tak naprawdę komunikacja może być inicjowana tylko w jedną stronę. Dopiero kiedy sami o coś zapytamy (wyślemy request), serwer ma okazję odpowiedzieć. To duża wada. Często byłoby lepiej, aby to on sam informował, że sytuacja się zmieniła i np. należy coś zaktualizować.
Skorzystałaby na tym na pewno nasza strona festiwalu, ale cóż... AJAX tego nie potrafi.
Aby lepiej zobrazować to, o czym mówimy, posłużymy się przykładem.
Wyobraź sobie średniej wielkości software house, gdzie przydziałem zadań zajmuje się jedna, zamknięta swoim biurze osoba. Powiedzmy, że jest nią PM (Project Manager). Pozostała część zespołu to szeregowi developerzy, z których każdy pracuje w swoim biurze.
Załóżmy, że PM ustala rano taski na dany dzień. Wtedy też każdy developer przychodzi do jego biura, a zadania zostają rozdzielone. Następnie wszyscy programiści udają się do swoich stanowisk i pracują tam sami do końca dnia. Jeśli potrzebują o coś zapytać, po prostu odwiedzają PM-a. On sam nigdy nie opuszcza biura.
Czy w takim scenariuszu, to że przez praktycznie cały dzień pracownicy nie mają kontaktu z PM-em jest jakimś problemem? Niespecjalnie. Tak naprawdę ciągła komunikacja nie byłaby tu potrzebna. Nawet to, że PM nigdy sam nie ruszy się ze swojego biura, nikomu nie przeszkadza.
Owszem, poranna audiencja u PM-a, kiedy odwiedzają go wszyscy developerzy, może być dla niego przez chwilę męczącą, ale w gruncie rzeczy praca przebiega prawidłowo. Programiści przychodzą dodatkowo o różnych porach (8:05, 8:15, 9:00), więc nie jest to zbyt dużym problemem.
Teraz wyobraźmy sobie drugi scenariusz. PM nadal ma własne biuro, z którego się nie rusza, a developerzy pracują w swoich. Taski nie są jednak układane rano przed pracą, lecz PM zajmuje się ich tworzeniem na bieżąco podczas całego dnia. Do tego ciągle rozmawia przez telefon ze zleceniodawcą i nie można się do niego dodzwonić. Tym samym developerzy są zmuszeni, co jakiś czas opuszczać swoje biuro i zaglądać do PM-a z pytaniem, czy może pojawiło się już jakieś nowe zadanie. Oczywiście wiele takich podróży będzie bezowocnych.
Widzisz, jaki mamy tu problem? Ten system jest kompletnie niewydajny. Pracownicy, zamiast pracować, co chwilę spacerują, aby w większości przypadków usłyszeć "wybacz, nic jeszcze dla Ciebie nie mam". Sam PM też nie może spokojnie pracować, bo co chwila odwiedzają go developerzy i musi im odpowiadać.
Dostrzegasz tutaj analogię do AJAX-u? PM to nasz serwer, a developerzy to klienci. Oczywiście AJAX nie musi być zły, ale pamiętajmy o jego ograniczeniach.
WebSocket również służy – podobnie jak AJAX – do komunikacji klienta z serwerem, ale zarządza nią w trochę inny sposób.
Po pierwsze, połączenie jest ciągłe. Inicjuje się, gdy po raz pierwszy uruchomimy aplikację (np. otwierając ją w zakładce przeglądarki) i zamyka dopiero wtedy, kiedy zostanie ona wyłączona. Warto dodać, że każda kolejna zakładka to nowy klient (w WebSocket nazywany "socketem"), a więc nowy kanał komunikacyjny. W szczegóły zagłębimy się za chwilę.
Po drugie, kontakt może być inicjowany przez obie strony. Serwer nie musi oczekiwać na klienta, tylko sam jest w stanie coś mu przesłać.
Możesz zastanawiać się nad wydajnością takiego rozwiązania. Jak to jest skonstruowane? Klient i serwer ciągle wymieniają między sobą dane? Nie. WebSocket wykorzystuje znany Ci już koncept emitowania akcji i nasłuchiwania. Krótko mówiąc, komunikacja polega na wzajemnym nasłuchiwaniu się obu stron – kiedy jedna coś nada (wyemituje jakiś event), druga może jakoś zareagować. Przez dłuższy czas może nie dziać się nic, jednak kiedy tylko jedna ze stron wyśle w eter jakąś informację, druga od razu ją odbierze. Co ważne, komunikacja jest ciągła, więc wszystko odbywa się w czasie rzeczywistym.
Opowiedzmy teraz dokładnie, jak to wygląda krok po kroku.
Kiedy klient łączy się z serwerem, powstaje między nimi kanał komunikacyjny. Od tego momentu oba podmioty są ze sobą w ciągłym kontakcie. Nie muszą koniecznie ciągle czegoś przesyłać, niemniej jednak bez przerwy nasłuchują, czy drugi nie ma czegoś do powiedzenia.
Możesz zobrazować to sobie jako uścisk dłoni. Witasz się z kimś i od tej chwili jesteście w kontakcie – jeden słucha, co drugi ma do powiedzenia. Nie musicie mówić ciągle, ale nawet jeśli nastanie cisza, słuchacie i oczekujecie na to, co za chwilę powie druga osoba.
Spójrz na fioletową linię symbolizującą kanał komunikacyjny, nie znika ona nawet na sekundę. Klient i serwer są w ciągłym kontakcie.
Co mamy na myśli mówiąc "klient łączy się z serwerem"? Chodzi tutaj o rozpoczęcie pracy – kiedy otworzysz np. localhost:8000, na którym jest Twój serwer lokalny, to w tej chwili stajesz się "klientem", a WebSocket tworzy kanał komunikacyjny.
Po utworzeniu kanału komunikacyjnego obie strony są w trybie nasłuchu. Sami decydujemy, na co dokładnie serwer czy klient ma zwrócić uwagę, dodając nasłuchiwacze. Pomysł ten nie jest dla Ciebie niczym nowym, mówiliśmy o nich już od samego początku nauki JS-a. Dzięki nim serwer może np. oczekiwać na zdarzenie join, a po jego wykryciu, dodać użytkownika do bazy danych. Jeśli klient wyemituje akcję, to serwer od razu na nią zareaguje. Naturalnie, nawet gdybyśmy nie ustawili nasłuchiwacza, serwer też by ją wykrył, tylko zwyczajnie nic by z tym nie zrobił.
Oczywiście nie zapominajmy, że WebSocket to komunikacja w obie strony, więc nasłuchiwać na zdarzenie oraz emitować je może zarówno klient, jak i serwer.
Wciąż możemy pozostać przy analogii rozmowy. Przed spotkaniem obie strony mogą spodziewać się, o co zapyta druga osoba i przygotować właściwe odpowiedzi. Wyobraź sobie np. rozmowę o pracę. Możesz przed nią założyć, że jeśli tylko usłyszysz pytanie o zarobki, postarasz się nie zejść poniżej założonej kwoty, a potem szybko zmienisz temat, aby ta informacja nie zwaliła rekrutera z nóg ;)
Taka rozmowa byłaby konwersacją w stylu emitowanie/nasłuchiwanie. Od początku rozmowy zakładasz, że może paść pytanie o zarobki i w razie czego masz gotową odpowiedź.
Oczywiście pozostając przy analogii do WebSocketów, rekruter również mógłby przygotować się wcześniej na kilka pytań z Twojej strony.
Warto dodać jeszcze dwie rzeczy. Po pierwsze, nie musimy emitować tylko "suchej" akcji (np. join). Wraz ze zdarzeniem możemy wysyłać również jakieś informacje, np. użytkownik wpisuje login, a następnie emituje zdarzenie join wraz z danymi { login: 'JohnDoe' }. Dzięki temu serwer będzie wiedział o kim mowa.
Po drugie, choć łączność jest nieco bardziej zaawansowana niż przy użyciu AJAX-u, jedna rzecz się nie zmienia – WebSocket to tylko komunikacja klient-serwer. Nie możemy łączyć się bezpośrednio pomiędzy samymi socketami (klientami). Do tego musimy użyć serwera jako pośrednika. O tym, jak dokładnie możemy to osiągnąć, opowiemy jednak trochę później.
Na koniec mały przykład:
Tutaj sytuacja jest następująca:
join, a klient na userAdded. Oba podmioty są w ciągłym kontakcie.join (wraz z danymi o użytkowniku), serwer od razu reaguje. Z racji tego, że oczekiwał na ten event, gdy tylko dowiaduje się, że on nastąpił, wykonuje wcześniej założone instrukcje, a więc – dodaje użytkownika do swojej tablicy z danymi i emituje w drugą stronę zdarzenie userAdded (tym razem bez żadnych danych).Gdy jedna ze stron się rozłączy (np. zamknie zakładkę aplikacji klienta), kanał komunikacyjny po prostu zniknie, a konwersacja dobiegnie końca.
Pamiętasz naszą analogię z firmą, którą przedstawiliśmy, aby zobrazować ograniczenia AJAX-u?
Przypomnijmy, jak wyglądał scenariusz B. PM był cały dzień zamknięty w biurze, a developerzy pracowali w swoich. Między nimi nie było żadnej bezpośredniej komunikacji. Aby dowiedzieć się, czy jest coś do zrobienia (PM przygotowuje taski na bieżąco), programiści musieli osobiście fatygować się co jakiś czas do PM-a. Co ważne, bardzo często te podróże były bezowocne. Odwzorowało to ograniczenia komunikacji AJAX-a, w której to klient zawsze musiał inicjować requesty.
Jak moglibyśmy rozwiązać taki problem w analogii do WebSocketów?
Wyobraź sobie, że w każdym biurze zamontowano głośnik i mikrofon. Teraz programiści mogliby spokojnie pracować albo czekać na zadania u siebie. Gdyby PM coś dla nich przygotował, poinformowałby ich za pomocą mikrofonu (analogia do serwera emitującego event). Słysząc komunikat, programista odpowiednio by zareagował (nasłuchiwanie na event). Co ważne jednak sam także łatwo mógłby w każdej chwili dopytać PM-a o jakieś szczegóły (klient też może emitować event).
Oczywiście wciąż byłaby to komunikacja PM–>programista (serwer–>klient) albo programista–>PM (klient–>serwer) i nie byłoby kontaktu między samymi developerami (klientami). W każdej chwili mogliby jednak skorzystać z pośrednictwa PM-a, tak aby w razie potrzeby przekazywał wiadomości od jednego do drugiego (serwer jako pośrednik między socketami).
Przyznaj, taka komunikacja wygląda o wiele bardziej obiecująco :)
Na koniec podsumujmy oba rozwiązania.
AJAX to:
WebSocket to:
Czy możemy powiedzieć, że któryś jest lepszy lub gorszy? Nie. Tak naprawdę, wszystko zależy od sytuacji. Czasem wystarczy AJAX, bo WebSocket tylko skomplikowałby nasz kod. Innym razem AJAX nie podoła, a WebSocket będzie idealny. Jak zawsze, ostateczny wybór należy do nas.
Dość teorii. Czas na wykorzystanie technologii WebSocket w praktyce. Aplikacją, który razem wykonamy, będzie prosty czat. To flagowy przykład użycia WebSocketów – potrzebujemy bowiem szybkiej wymiany danych, najlepiej w czasie rzeczywistym, ważna jest też spora wydajność.
Efektem będzie aplikacja czatu działająca w następujący sposób.
/ powinien zwracać naszą aplikację klienta.Nie wchodzimy tu jeszcze w szczegóły, ale z samego opisu możesz już wywnioskować, że komunikacja pomiędzy serwerem i klientem będzie opierać się na technice emitowania i nasłuchiwania akcji.
W przypadku czatów publicznych bardzo popularną koncepcją jest niepokazywanie archiwalnych wiadomości. Użytkownik nie wie, co było pisane na danym kanale przed jego przyjściem. My też zastosujemy się do takiego pomysłu.
Na końcu aplikacja powinna wyglądać następująco:
Nie martw się wyglądem, style zostaną Ci dostarczone, tak byśmy mogli skupić się na stworzeniu samej logiki aplikacji.
Na początek przygotujemy strukturę HTML i podstawową logikę JS.
Zacznij od założenia katalogu projektu, a w nim folderu client. Tutaj będziemy przechowywać naszą aplikację klienta.
Następnie utwórz w nim trzy pliki: index.html, style.css oraz app.js.
Plik index.html będzie miał następująco strukturę:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Chat App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main id="app">
<!-- HEADER -->
<header>
<h1>Chat.app</h1>
</header>
<!-- WELCOME FORM -->
<form id="welcome-form" class="welcome-form show">
<h2>Log in</h2>
<input class="text-input" type="text" id="username" placeholder="Type your name here">
<button class="btn" type="submit">Join</button>
</form>
<!-- MESSAGES SECTION -->
<section class="messages-section show" id="messages-section">
<h2>Messages</h2>
<!-- messages list -->
<ul class="messages-section__list" id="messages-list">
<li class="message">
<h3 class="message__author">John Doe</h3>
<div class="message__content">
Hello!
</div>
</li>
<li class="message message--self">
<h3 class="message__author">You</h3>
<div class="message__content">
Hi!
</div>
</li>
<li class="message message--self message--received">
<h3 class="message__author">You</h3>
<div class="message__content">
It's great to see you again!
</div>
</li>
</ul>
<!-- messages form -->
<form id="add-messages-form">
<input class="text-input" type="text" placeholder="Type your message here" id="message-content">
<button class="btn" type="submit">Send</button>
</form>
</section>
</main>
<script src="app.js" type="module"></script>
</body>
</html>
Mamy tu jeden prosty formularz do logowania (#welcome-form), a drugi do wysyłania wiadomości (#add-messages-form), oraz samą listę (#messages-list). Oczywiście niedługo będziemy zajmować się odpowiednią obsługą tych elementów. Klasy zostały nadane w celu łatwego ostylowania (zaraz dodamy arkusz), a identyfikatory (id) ułatwią dostęp do elementów, gdy zaczniemy pisać kod JS.
Arkusz style.css powinien wyglądać następująco:
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');
body {
background: #282c34;
color: #fff;
font-family: 'Open Sans', sans-serif;
padding: 30px;
text-align: center;
}
h1 {
font-weight: lighter;
}
p {
color: #999;
font-size: 0.8rem;
}
.btn {
background: #0064e7;
border: none;
border-radius: 15px;
padding: 10px 25px;
color: #fff;
font-family: inherit;
margin: 20px 10px;
text-transform: uppercase;
outline: none;
transition: .2s;
cursor: pointer;
}
.btn:hover {
box-shadow: 0 0 20px rgba(0,0,0,0.2);
}
.text-input {
background: none;
border: none;
color: #fff;
border-bottom: 2px solid #0064e7;
padding: 8px 25px 8px 5px;
font-family: inherit;
outline: none;
opacity: 0.6;
transition: .3s;
}
.text-input:focus {
opacity: 1;
}
.welcome-form {
display: none;
}
.messages-section {
margin-bottom: 100px;
display: none;
}
.messages-section__list {
max-width: 400px;
list-style-type: none;
padding: 0;
margin: 20px auto 40px auto;
}
.message {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.message--self {
align-items: flex-end;
}
.message--received {
animation: fadeIn;
animation-duration: 3s;
}
.message__author {
font-size: 0.8rem;
font-weight: bold;
color: #999;
text-align: right;
}
.message__content {
background: rgb(68, 68, 68);
border: none;
border-radius: 15px 5px 15px 15px;
padding: 10px 25px;
color: #fff;
font-size: 0.9rem;
max-width: 200px;
}
.message--self .message__content {
background: #3498db;
}
.show {
display: block;
}
@-webkit-keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
Nie musisz specjalnie wczytywać się w ten plik, chodzi nam tylko o to, by aplikacja jakoś się prezentowała. Sama sprawa doboru odpowiednich klas nie nastręczy wielu problemów – w pliku HTML są już zastosowane na przykładowych danych, więc będziemy mieli na czym się wzorować.
Zwróćmy uwagę na trzy klasy, które będą dla nas bardzo istotne.
.show – to znana Ci już technika. Nasze dwa podstawowe elementy, a więc formularz powitalny i sekcja z wiadomościami, są domyślnie ukryte. Nadanie tej klasy pozwoli na ich pokazanie, a jej zabranie spowoduje, że elementy staną się ponownie niewidoczne..message--self – lekko modyfikuje podstawowy styl wiadomości i odróżnia ją od tych wysłanych przez innych użytkowników..message--received – użyta na wiadomości pokazuje ją z opóźnieniem, z efektem "fadeIn" (płynne wejście).Na tym etapie, po uruchomieniu w przeglądarce pliku index.html, powinniśmy zobaczyć następujący widok:
Na razie nie będziemy jeszcze używać WebSocketów, przygotujemy tylko bazową logikę. Nie spotkasz się więc z niczym nowym, a cały kod, który teraz napiszemy, powinien być już dla Ciebie łatwy do implementacji i zrozumienia.
Otwórz plik app.js i zacznij od przygotowania referencji do HTML-a.
Ustaw następujące stałe:
loginForm – referencja do formularza logowania.messagesSection – referencja do sekcji z wiadomościami.messagesList – referencja do samej listy wiadomości.addMessageForm – referencja do formularza dodawania wiadomości.userNameInput – referencja do pola tekstowego z formularza logowania.messageContentInput – referencja do pola tekstowego z formularza do wysyłania wiadomości.Możesz wybrać je bezpośrednio po przygotowanych w HTML-u id.
Jak myślisz, przydadzą nam się jakieś zmienne globalne? Na pewno można w ten sposób przechowywać login użytkownika, aby mieć do niego łatwy dostęp w każdym miejscu aplikacji, a nie tylko na etapie sprawdzania, co wpisano w pole tekstowe. Będziemy potrzebować go np. przy funkcji służącej do wysyłania wiadomości. W końcu musimy powiedzieć serwerowi nie tylko co wysyłamy, ale też kim jesteśmy.
Dodaj więc nową zmienną globalną. Nazwij ją userName.
A co, jeśli chodzi o wiadomości? Czy przyda nam się jakaś tablica, w której przechowywalibyśmy aktualną ich listę? Mówiliśmy już, że serwer zawsze będzie informował tylko o nowych wiadomościach. Kiedy więc wchodzisz na czat, nie masz dostępu do wpisów, które pojawiały się przed Twoim dołączeniem. Wydaje się więc, że przetrzymywanie tych danych w tablicy jest zbędne. Po otrzymaniu informacji o nowej wiadomości możemy dodać ją bezpośrednio do HTML-a i nie potrzebujemy jej magazynować w JS-ie.
Na starcie cała sekcja messages (lista i formularz do dodawania wiadomości) powinna być ukryta. Nie chcemy bowiem, aby użytkownik mógł wysyłać wiadomości jako bezimienny.
W tej chwili działa na nią klasa .show, więc musisz ją odebrać temu elementowi. Możesz to zrobić bezpośrednio w pliku HTML.
Oczywiście na późniejszym etapie nadamy klasę .show temu elementowi, ale dopiero wtedy, kiedy ustalimy już, jaki login wybrał użytkownik.
Czas na pierwszy formularz – #loginForm, który pozwala na wpisywanie nazwy użytkownika.
W tej chwili po kliknięciu na button "Join" strona się odświeża. Pamiętasz dlaczego? To po prostu domyślne zachowanie – kliknięcie na button równa się wysłaniu formularza. Teraz tego nie chcemy, dlatego musisz zadziałać wbrew założeniom twórców rozwiązania.
Dodaj w app.js nasłuchiwacz na ten formularz. Po wykryciu zdarzenia submit powinien on odpalać funkcję login. Stwórz ją. Funkcja ta powinna przyjmować informacje o evencie oraz blokować domyślne zachowanie przeglądarki (preventDefault).
Następnie rozwiniemy funkcję login. Dodaj do niej walidację wartości userNameInput. Kod powinien sprawdzać, czy pole nie jest puste. Jeśli tak, należy zwrócić komunikat o błędzie (wystarczy zwykły alert). Gdy wszystko w porządku, nasz kod powinien przypisać wartość tego pola do zmiennej userName, schować formularz logowania oraz pokazać sekcję wiadomości. Pamiętaj, że mechanizm chowania/pokazywania tych elementów w naszej aplikacji opiera się na obecności klasy .show.
Efektem Twoich działań w tej chwili powinno być poprawne obsługiwanie buttonu "Join".
Tym razem zajmiemy się formularzem #addMessageForm, który ma służyć dodawaniu wiadomości.
Ponownie zaczniemy od zablokowania domyślnego zachowania przeglądarki. Do eventu submit dodaj nasłuchiwacz, który uruchomi funkcję sendMessage. Ta ostatnia powinna przyjmować informacje o evencie oraz blokować domyślne zachowanie przeglądarki.
Co należy zrobić, jeśli wiemy, że użytkownik chce wysłać wiadomość? Najpierw sprawdźmy, czy w ogóle coś wpisał. Dodaj więc walidację pola tekstowego w tym formularzu i jeśli nie ma w nim treści, pokaż użytkownikowi alert z odpowiednim komunikatem. Gdy wszystko jest w porządku, wywołaj funkcję addMessage, a jako argument przekaż jej wartość userName, oraz wpisaną treść #messageContentInput (addMessage(userName, messageContentInput.value)). Na końcu wyczyść to pole tekstowe.
Na tym zakończyliśmy budowę funkcji sendMessage w jej prostym wydaniu. Oczywiście później dodamy do niej jeszcze kilka elementów, będziemy np. informować serwer, że pojawiła się nowa wiadomość i trzeba rozesłać ją reszcie użytkowników.
Co do funkcji addMessage, na razie ma przyjmować informację o autorze wiadomości oraz jej treści i generować odpowiedni kod HTML, czyli po prostu dodać nowy element li do naszej listy z wiadomościami. Musi przy tym zachować odpowiedni format (nagłówek to autor, treść ma być w divie o klasie .message itd.).
Funkcja addMessage ma bezpośredni dostęp do globalnej zmiennej userName, dlaczego więc ją przekazujemy? Robimy tak z prostego powodu. Funkcja addMessage ma pozwalać na dodawanie wiadomości od różnych osób i nie zakłada z góry, że będzie to tylko userName. Ona po prostu oczekuje, że dostanie tę informację z zewnątrz, właśnie w formie argumentu. Może to być faktycznie wartość otrzymana z userName, ale równie dobrze całkiem inna (np. z serwera wraz z treścią wiadomości). Funkcja ta nie jest zainteresowana, skąd pochodzą dane – z pola tekstowego, ze zmiennej globalnej, czy może z odpowiedzi serwera. Ona tylko je dostaje i generuje element HTML.
Stwórz teraz tę funkcję (koniecznie przygotuj dwa parametry – author i content), a następnie dodaj do niej logikę zgodną z poniższym planem:
li i dodaj go do stałej message..message oraz .message--received, która zadba o eleganckie wyłonienie się elementu.author jest równe zmiennej globalnej userName, to dodaj do li również klasę .message--self. Będzie ona rozróżniała nasze wiadomości od tych wysłanych przez inne osoby.h3 o klasie message__author i diva o klasie message__content. Skąd wiemy, że akurat tak ma wyglądać struktura li? Po prostu w naszym pliku index.html mamy już przykładowe wiadomości, które wskazują, jak powinna być zbudowana ta struktura.author lub You (jeśli author jest równe userName).content.message do #messagesList.Efekt powinien być następujący:
function addMessage(author, content) {
const message = document.createElement('li');
message.classList.add('message');
message.classList.add('message--received');
if(author === userName) message.classList.add('message--self');
message.innerHTML = `
<h3 class="message__author">${userName === author ? 'You' : author }</h3>
<div class="message__content">
${content}
</div>
`;
messagesList.appendChild(message);
}
Po wszystkim usuń wpisane "na sztywno" wiadomości z HTML-a.
Od tej chwili nasza aplikacja powinna działać jak na poniższej animacji:
Być może udało Ci się zauważyć, że w jednym i drugim polu tekstowym, przeglądarka stara się podpowiedzieć, co chcemy napisać. To ogólnie dość pomocna cecha formularzy, zwłaszcza jeśli chodzi np. o wpisywanie loginów czy e-maili, w czacie jednak raczej chcielibyśmy się jej pozbyć. Możesz użyć do tego atrybutu autoComplete. Jeśli ustawisz na inpucie jego wartość jako off, przeglądarka przestanie nam "podpowiadać".
Nasz klient jest już prawie gotowy na wprowadzenie WebSocketów. Potrzebujemy jeszcze jednej ważnej rzeczy.
Czas na podstawową wersję serwera. Przypomnijmy sobie, jaką ma pełnić rolę w naszej aplikacji:
/ powinien zwracać naszą aplikację klienta.Oczywiście trzeciego punktu na razie nie wykonamy, bo to już funkcjonalność, którą oprzemy na WebSocketach. Możemy jednak spokojnie zająć się dwoma pierwszymi zadaniami.
Podobnie jak w przygotowaniach klienta, nie będziemy Ci bardzo pomagać, ani przeszkadzać ;) Wskażemy tylko, co powinno zostać wykonane, bo nie ma tu na razie żadnych nowości.
Zacznij od utworzenia szkieletu serwera – package.json i pliku server.js (w głównym katalogu projektu). Następnie pobierz Express i tak zmodyfikuj server.js, aby po jego uruchomieniu tworzył serwer lokalny pod portem 8000. Jeśli chcesz, możesz też na tym etapie pobrać Nodemona (pamiętaj, by zainstalować go jako zależność projektu, a nie globalnie: npm install --save-dev nodemon) i dodać do package.json task, który uruchamiałby pod start nasz server.js z jego pomocą – nodemon server.js.
Następnie zastanów się nad podstawowym middleware. Czy któreś z nich Ci się przyda?
Pokaż podpowiedź Ukryj podpowiedź
Nasz serwer nie będzie prawdopodobnie obsługiwał żadnych formularzy, nie potrzebujemy więc tych middleware, które miałyby nam przy nich pomagać. Serwer i klient będą do tego spójną aplikacją, więc nie przyda się również żadna pomoc przy CORS.
Tutaj komentarz jest zbędny. Dodaj do serwera kod inicjujący tablicę messages.
/Czas na dodanie funkcjonalności pokazywania aplikacji umieszczonej w /client pod głównym linkiem. Jak możemy to osiągnąć?
Przypomnij sobie poprzedni moduł i sytuację, kiedy musieliśmy rozwijać nasz serwer API, aby pod linkiem / pokazywał aplikację reactową z folderu client/build. Możesz potraktować tamto zadanie jako ściągę.
Pokaż podpowiedź Ukryj podpowiedź
Musisz stworzyć po prostu endpoint wyłapujący wszystkie linki i renderujący plik index.html z ./client.
Dodatkowo pamiętaj, że nasza aplikacja korzysta też z plików zewnętrznych (style.css oraz app.js), musimy więc dodać middleware, który pozwoli na ich udostępnianie przez serwer. Nie musisz robić pojedynczych endpointów do ich obsługi. Wystarczy, że użyjesz wbudowany middleware express.static. Pamiętasz go? W razie problemów znowu odsyłamy do poprzedniego modułu.
To wszystko. Uruchom teraz serwer i sprawdź, czy faktycznie pod adresem localhost:8000 pokazuje naszą aplikację. Jeśli tak, to połowę zadania mamy już z głowy :)
Szkielet jest już gotowy. Mamy podwaliny serwera, który pod głównym linkiem otwiera naszą aplikację klienta. Sam klient też potrafi już całkiem sporo – obsługuje formularze, ma również gotową funkcję, która dokleja kolejne wiadomości do listy. Właściwie brakuje nam tylko jednego – komunikacji między klientem a serwerem. Tym zajmiemy się już w kolejnym submodule. W końcu wykorzystamy WebSockety w praktyce!
Stwórz repozytorium, prześlij wszystkie zmiany i opublikuj repo na GitHubie. Link wyślij swojemu mentorowi.
Na tym etapie:
/client,Przyszedł czas, by w końcu nawiązać nić porozumienia między klientami a serwerem. W zadaniu tym pomogą, na razie głównie teoretycznie, WebSockety.
Zanim przejdziemy do realizacji, zastanówmy się, jak powinien zachowywać się nasz serwer i klient po zmianach. Do czego w ogóle dążymy?
Zgodnie z założeniami, klient musi bezustannie nasłuchiwać na akcję nowej wiadomości (nazwijmy ją message). Jeśli wykryje takie zdarzenie, powinien dodać nowy element li z treścią tej wiadomości do listy. Oprócz tego klient powinien być też w stanie sam emitować akcję message (wraz z treścią), kiedy użytkownik doda nową wiadomość – tak, aby poinformować serwer, że pojawiło się coś nowego.
Zatem jakie nasłuchiwacze i emittery powinniśmy przygotować?
Nasłuchiwacze:
message – gdy klient wykryje to zdarzenie, odbierze nową wiadomość (w założeniu będzie ona otrzymywana od serwera wraz z samym zdarzeniem) i doda ją do HTML-a.Emittery
message – klient powinien emitować to zdarzenie, gdy dodaje własną wiadomość i chce o tym poinformować serwer. Oczywiście, koniecznie musimy wraz z samą akcją informować, kto wysyła tę wiadomość i jaka jest jej treść.Zgodnie z założeniem, serwer powinien również bezustannie nasłuchiwać na message. Kiedy tylko wykryje to zdarzenie, musi dodać wpis do swojej tablicy (tak, abyśmy po prostu mieli gdzieś log wszystkich wiadomości) oraz przekazać tę wiadomość dalej (wyemitować ją do pozostałych użytkowników).
Już wcześniej powiedzieliśmy, że WebSocket nie zmienia jednej rzeczy – wciąż możemy komunikować się tylko w relacji klient-serwer i nie mamy możliwości bezpośredniej łączności pomiędzy samymi klientami. Nie jest to jednak bardzo uciążliwie, bowiem serwer w WebSocketach całkiem nieźle radzi sobie w roli pośrednika.
Przy okazji, jedna sprawa. Wiemy, że serwer będzie u nas pełnił rolę pośrednika w przekazywaniu postów. Zatem ktoś wysyła wiadomość, serwer natychmiast ją wykrywa (w końcu ciągle nasłuchuje na taką możliwość) i przesyła ją dalej do innych. Mówiliśmy też, że nowy użytkownik nie będzie otrzymywał archiwalnych wiadomości, a na jego liście będą pojawiały się tylko nowe.
Po co więc stworzyliśmy na serwerze tablicę messages? W jakim celu chcemy przechowywać wiadomości, skoro na dobrą sprawę dalibyśmy sobie radę bez tej funkcjonalności? Chcieliśmy pokazać Ci, że serwer może być tutaj czymś więcej niż tylko pośrednikiem – jest w stanie np. przechowywać jakieś dane, kontrolować to, co przepływa pomiędzy klientami. Potraktuj tę tablicę jako np. log wszystkich wiadomości, do którego zawsze może zajrzeć administrator czatu.
Wiemy już mniej więcej, jak muszą wyglądać klient oraz serwer po zmianach. Klient będzie ciągle nasłuchiwał na message. Jeśli wykryje to zdarzenie, doda do swojej listy nową wiadomość, zgodnie z informacjami, które otrzyma wraz z samym eventem. Równocześnie, gdy będzie dodawać wiadomość, sam wyemituje event message do serwera, przesyłając przy tym informację o treści wiadomości oraz swoim loginie. Serwer za to będzie ciągle nasłuchiwał na event message i kiedy go wykryje, dopisze wiadomość do swojej tablicy oraz przekaże ją dalej do pozostałych klientów.
Plan jest prosty? Wydaje się, że tak :) Czas na praktykę!
Zaczniemy od dodania do naszego klienta paczki Socket.IO. Na rynku jest również kilka innych rozwiązań, ale ta cieszy się największą popularnością. W teorii nie potrzebujemy żadnego pakietu do działania WebSocketów, jednak komfort pracy, który zaoferuje nam Socket.IO jest wart zachodu.
W pliku HTML dodaj następujący import:
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js"></script>
Co prawda, w "normalnej" aplikacji pewnie skorzystalibyśmy z Webpacka lub Parcela i użyli komendy import wewnątrz app.js, nie będziemy jednak na siłę komplikować naszego prostego przykładu i skorzystamy z CDN-a.
Teraz czas na zainicjowanie naszego klienta (socketa). Wejdź do pliku app.js i dodaj poniższy kod (na razie wstrzymaj się jeszcze z testami, wszystko zadziała, gdy serwer będzie zaimplementowany).
const socket = io();
Potraktuj to, jak rozkaz dla JS-a: zainicjuj nowego klienta socketowego i zachowaj referencje do niego pod stałą socket. Naturalnie nazwa stałej może być zupełnie inna, socket to tylko nasz pomysł, który na pewno ułatwi utrzymywanie porządku w kodzie.
Domyślnie Socket.io zakłada, że serwer, z którym będziemy się komunikować to ten sam, na którym odpalany jest klient (u nas to localhost:8000). Istnieje jednak możliwość zadeklarowania innego adresu. Używamy do tego funkcji connect.
socket.connect("http://example.com");
Drugim domyślnym ustawieniem, które możemy zmienić, jest automatyczne połączenie. Gdy tylko tworzymy socket, łączy się on z wybranym adresem (domyślnym bądź ustawionym przez nas ręcznie). By to zmienić, wystarczy, że przy inicjacji ustawimy opcję autoConnect na false.
const socket = io({
autoConnect: false
});
W takiej sytuacji to my sami decydujemy, kiedy kanał komunikacyjny z serwerem zostanie otwarty. Możemy inicjować go za pomocą komendy open (np. socket.open()).
Czas na dodanie nasłuchiwacza na event message, który w przypadku wykrycia tego zdarzenia spowoduje dodanie wiadomości w HTML-u. Czy ta funkcjonalność jest Ci znajoma? Tak, dokładnie to robi nasza funkcja addMessage. Nasłuchiwacz będzie więc wyglądał następująco:
socket.on('message', addMessage)
Przyznaj, że to dość proste :) Tak właśnie dodajemy nasłuchiwacze w Socket.IO:
socket('event-name', func-to-call)
Pozostaje jeszcze kwestia danych otrzymywanych wraz z samą informacją o zdarzeniu. Mówiliśmy, że serwer będzie emitował message wraz z informacją o autorze i treści tej wiadomości.
To, co dokładnie będzie przekazane w evencie, zależy oczywiście od emitującego. Niemniej jednak załóżmy, że serwer zawsze będzie wysyłał wraz z tym eventem po prostu obiekt w formacie { author: 'John Doe', content: 'Lorem Ipsum' }. Skąd mamy pewność, że serwer właśnie tak będzie emitował ten event? No cóż... W końcu sami piszemy jego logikę ;)
Nasz nasłuchiwacz mógłby więc wyglądać tak:
socket.on('message', (event) => addMessage(event.author, event.content))
Ten kod możemy czytać jak rozkaz: obserwuj serwer, czekając na zdarzenie message, jeśli je wykryjesz, odpal funkcję, która ma przyjąć przesyłane przez serwer dane jako event, a następnie wykorzystaj je do odpowiedniego wywołania funkcji addMessage. Wiemy, że funkcja addMessage dalej zrobi wszystko, jak trzeba, bo testowaliśmy ją już wcześniej lokalnie.
Naturalnie powyższy kod można łatwo skrócić z wykorzystaniem destrukturyzacji:
socket.on('message', ({ author, content }) => addMessage(author, content))
Gdzie umiejscowić ten kod? Najlepiej dość wysoko, aby łatwo można go było odnaleźć. Jeśli w przyszłości pojawią się kolejne nasłuchiwacze, to dla porządku warto trzymać je wszystkie obok siebie.
Czas na dodanie emittera, chcemy bowiem, aby wiadomość nie tylko pojawiała się na naszej lokalnej liście (to już u nas działa), ale żeby informację o niej otrzymywał serwer.
Emitowanie akcji będzie równie intuicyjne, jak dodawanie nasłuchiwaczy. Wygląda to następująco:
socket.emit('event-name', possible-data);
Musimy powiedzieć, jaki event chcemy emitować oraz ustalamy jakie dane wraz z nim wysyłamy, przy czym akurat drugi parametr jest opcjonalny. Możemy wysyłać w eter również eventy bez danych:
socket.emit('event-name');
W naszym przypadku jednak prześlemy zarówno event, jak i dane, np.
socket.emit('message', { author: 'John Doe', content: 'Lorem Ipsum' });
Przejdźmy do konkretów – jak to będzie wyglądało u nas? Samo emitowanie message powinno odbywać się po kliknięciu na przycisk "Send" w formularzu wiadomości, a więc w funkcji sendMessage. Najlepiej ustawić je po wywołaniu addMessage. Jeśli chodzi o dane, to wartość author pobierzemy z userName, a jako content możemy od razu użyć messageContentInput.value.
Po zmianach funkcja sendMessage powinna wyglądać mniej więcej tak:
function sendMessage(e) {
e.preventDefault();
let messageContent = messageContentInput.value;
if(!messageContent.length) {
alert('You have to type something!');
}
else {
addMessage(userName, messageContent);
socket.emit('message', { author: userName, content: messageContent })
messageContentInput.value = '';
}
}
Jak widzisz, wbrew pozorom, nie było to aż tak trudne. Nasz klient jest już gotowy do komunikacji z serwerem, choć ten na razie jeszcze milczy. Zaraz to zmienimy.
Zaczniemy od dodania do naszego serwera paczki Socket.IO. Możemy użyć Yarna.
yarn add socket.io@2.2.0
To, że pakiet jest już pobrany, oczywiście nie wystarczy. Musimy go jeszcze zaimportować w server.js.
const socket = require('socket.io');
Zintegrujmy teraz z naszym serwerem możliwości oferowane przez tę paczkę.
const io = socket(server);
Oczywiście powyższy kod zakłada, że serwer jest zapisany u Ciebie właśnie pod stałą server. Jeśli używasz innej nazwy, np. serv, wpisz właściwą.
Metoda express() nie uruchamia Twojego serwera. Jest on tworzony dopiero po ustaleniu portu, czyli po użyciu metody listen.
Nie możemy więc zrobić tak:
const app = express();
const io = socket(app);
Zamiast tego chodzi nam o gotowy serwer, a więc np.
const app = express();
const server = app.listen(8000, () => {
console.log('Server is running on Port:', 8000)
});
const io = socket(server);
Od tej chwili nasz serwer powinien być już "podatny" na komunikację z klientami. Na razie jeszcze nie będzie na nic reagować – w końcu nie ustawiliśmy tutaj żadnych nasłuchiwaczy.
Przypomnijmy, jak ma to wyglądać na naszym serwerze. Tworzymy nasłuchiwacze na message, a wtedy serwer odbiera informację o nowej wiadomości (treść i autor), zapisuje ją w tablicy i przy pomocy emittera wysyła dalej do pozostałych klientów.
Tylko że od razu pojawia się pewien problem. W kliencie sytuacja była prosta, bo miał on tylko możliwość komunikacji z jednym podmiotem – serwerem. Ustawienie nasłuchiwaczy było łatwe, bez konieczności wskazywania o kogo chodzi, bo wiadomo, że muszą się tyczyć właśnie serwera. Tutaj jest inaczej – może być jeden klient, z którym serwer ma kontakt, ale równie dobrze może ich być setka. Należy więc ustawiać nasłuchiwacze na każdego z klientów z osobna.
Na szczęście nie będzie to aż takie trudne. Kiedy uruchamiamy klienta websocketowego, a więc chociażby odpalamy komendę const socket = io();, automatycznie emituje on do serwera akcję connection. Ma ona informować serwer, że pojawił się właśnie nowy podmiot do obsługi. Co ważne, przy tej okazji wysyłana jest też informacja o tym kliencie.
Oprócz eventu connection automatycznie wysyłanego przy inicjacji połączenia przez socket, istnieje jeszcze drugi analogiczny – disconnect. On także jest uruchamiany automatycznie, tylko że w momencie zamknięcia połączenia (czyli gdy np. użytkownik wyłącza zakładkę z naszą aplikacją).
Wystarczy więc, że kiedy serwer będzie wykrywał takie połączenie, nada nowemu klientowi potrzebne nasłuchiwacze. Czy jest to dla Ciebie jasne? Jeśli nie do końca, spójrz na ten kod:
io.on('connection', (socket) => {
socket.on('message', doSomething);
});
Możemy to rozumieć jak następujący rozkaz dla JS-a: kiedy wykryjesz nowe połączenie (nowego socketa), nasłuchuj na nim na zdarzenie message, przy czym, jeśli je wykryjesz, uruchom funkcję doSomething.
Zamysł jest więc prosty. Serwer od uruchomienia będzie ciągle nasłuchiwał na możliwość pojawienia się nowego klienta/socketu i gdy tylko wykryje, że ktoś się pojawił, będzie indywidualnie obserwować ten podmiot i oczekiwać na jakieś zdarzenie z jego strony. Owszem, na każdego ustawimy takie same nasłuchiwacze, ale każdy będzie obserwowany z osobna.
Pamiętasz naszą analogię z firmą? Tu też możemy się nią posłużyć. Powiedzmy, że PM ma założone z góry zadanie, aby obserwować pracę developerów i jeśli któryś z nich przestanie sobie radzić, musi coś z tym zrobić.
Jak by to wyglądało w praktyce? Kiedy tylko jakiś developer wszedłby do biura, nie musiałby nawet nic mówić (tak samo, jak socket nie musi nic "mówić", bo event connection jest nadawany z automatu). Gdy tylko PM (serwer) zauważy pracownika, będzie go bacznie obserwować, podobnie jak każdego kolejnego, który się pojawi. Ostatecznie stanie się więc wyczulony na to samo zdarzenie, czyli słabą pracę każdego developera z osobna.
Żeby lepiej zobrazować Ci, że faktycznie tak to wygląda, dodaj do serwera następujący kod:
io.on('connection', (socket) => {
console.log('New client! Its id – ' + socket.id);
socket.on('message', () => { console.log('Oh, I\'ve got something from ' + socket.id) });
console.log('I\'ve added a listener on message event \n');
});
Jak możemy go rozumieć? Gdy tylko pojawi się nowy klient (np. otworzymy zakładkę z naszą aplikacją), serwer to zauważy (wykryje event connection) i poinformuje nas o tym w konsoli. socket, jak mówiliśmy, to po prostu dostęp do samego klienta, a socket.id to unikalny identyfikator automatycznie nadawany każdemu przy inicjacji połączenia z serwerem. Czasami może być przydatny.
Po wypisaniu informacji o nowym kliencie serwer od razu zaczyna też oczekiwać na zdarzenie message, jeśli je wykryje, to wypisze w konsoli tekst "Oh, I've got something!".
Naturalnie każdy nowy klient (socket) to nowa wiadomość w konsoli i nowy nasłuchiwacz. Tak jak mówiliśmy już wcześniej, funkcja w connection będzie odpalana z osobna dla każdego klienta przy jego dołączeniu.
Sprawdźmy teraz w praktyce, jak to wygląda.
Uruchom swój serwer, a następnie wejdź w przeglądarce pod jego adres http://localhost:8000.
Co pojawi się w konsoli?
Zgodnie z przewidywaniami – serwer wykrył nowe połączenie (nowy socket), więc odpalił funkcję:
(socket) => {
console.log('New client! Its id – ' + socket.id);
socket.on('message', () => { console.log('Oh, I\'ve got something from ' + socket.id) });
console.log('I\'ve added a listener on message event \n');
});
Funkcja ta wypisała odpowiednie komunikaty w konsoli, ale też rozpoczęła nasłuchiwanie na event message na tym kliencie.
Otwórz teraz naszą aplikację w kolejnych dwóch lub trzech zakładkach. Co się okazuje?
Dla serwera każda zakładka to nowy klient i kanał komunikacyjny, wykrycie pozostałych uruchomiło więc ten sam proces. Serwer napisał w konsoli, że pojawili się kolejni klienci i na każdym z nich z osobna zaczął nasłuchiwanie na event message.
Przy okazji możesz zauważyć, że każdy nowy klient (socket), faktycznie otrzymał unikalne id.
Aby już ostatecznie udowodnić, że każdy klient jest traktowany indywidualnie, możemy zrobić jeszcze jeden test.
Gdy dodajemy wiadomość, klient powinien od razu emitować do serwera akcję message. Spróbuj dodać nową wiadomość na każdej z uruchomionych zakładek (a więc na każdym kliencie/sockecie).
Efekt powinien być mniej więcej taki:
Jak widzisz na grafice (albo i w Twoim własnym terminalu), serwer zawsze dokładnie wskazuje, skąd pochodzi wiadomość, oraz że uruchamia się właściwa funkcja przypięta do nasłuchiwacza konkretnego klienta. Gdy serwer wykrywa zdarzenie message np. na kliencie 1PAkNvZFEcNlF1epAAAA, uruchamia funkcję przypiętą do nasłuchiwacza, który obserwował właśnie ten socket. Widzimy więc, że sockety są obsługiwane indywidualnie, bo każdy z nich to własny nasłuchiwacz.
Przy okazji przekonaliśmy się też, że komunikacja klient->serwer już w naszej aplikacji działa ;)
Teraz możemy przetestować jeszcze jedną rzecz – automatyczny event disconnect. Powinien on być wysyłany przez klienta w momencie zamykania połączenia. Możemy to potraktować jako swoiste "pożegnanie".
Dodajmy obserwację tego eventu dla każdego z klientów:
(socket) => {
console.log('New client! Its id – ' + socket.id);
socket.on('message', () => { console.log('Oh, I\'ve got something from ' + socket.id) });
socket.on('disconnect', () => { console.log('Oh, socket ' + socket.id + ' has left') });
console.log('I\'ve added a listener on message and disconnect events \n');
});
Możesz przetestować teraz nasz serwer. Po zamknięciu zakładki z klientem powinien on informować nas w konsoli, że konkretny klient o konkretnym id nas opuścił.
Event ten może być naprawdę pomocny, kiedy np. przechowujemy na serwerze listę zalogowanych użytkowników i chcemy usuwać z niej odchodzących userów.
Nasz serwer potrafi już wykryć pojawienie się klienta, nasłuchiwać na event message, a nawet poinformować o ewentualnym zamknięciu połączenia.
Zgodnie z założeniami, powinien jeszcze dodać do tablicy messages wiadomość odebraną wraz z eventem oraz wyemitować tę akcję dalej, do pozostałych użytkowników (serwer jako pośrednik).
Wprowadźmy tę funkcjonalność.
Zacznijmy od odebrania danych od klienta (socketa). Wiemy, że nasz klient, emitując event message, wysyła wraz z nim informacje o autorze oraz samą treść wiadomości. Przypomnijmy, jak to wygląda w kliencie:
socket.emit('message', { author: userName, content: messageContent });
Faktycznie, dane są wysyłane, ale musimy je jeszcze odebrać w naszej funkcji przypiętej do nasłuchiwacza, a następnie dodać jako nowy element tablicy messages.
Zaktualizuj więc nasz nasłuchiwacz na event message w serwerze następująco:
socket.on('message', (message) => {
console.log('Oh, I\'ve got something from ' + socket.id);
messages.push(message);
});
Pozostaje nam jeszcze wyemitowanie tej wiadomości dalej, do pozostałych klientów. Jak możemy to zrobić?
Najprostszym wyborem byłoby użycie funkcji emit na obiekcie serwera.
io.emit('message', message);
U nas takie rozwiązanie się nie sprawdzi. io.emit emituje zdarzenie do wszystkich socketów, a więc dojdzie ono również do tego użytkownika, który sam tę wiadomość wysłał. Z kolei nasz klient działa tak, że zanim wyśle zdarzenie z wiadomością na serwer, wcześniej dodaje ją już do HTML-a. Gdyby odebrał informację o nowej (swojej) wiadomości, dopisałby ją do listy drugi raz.
Na szczęście Socket.IO udostępnia nam jeszcze inną możliwość, a mianowicie obiekt broadcast. Emitując zdarzenie z użyciem broadcastu (socket.broadcast.emit), nadajemy je do wszystkich socketów, oprócz tego, z którego go wysyłamy. To właśnie coś dla nas.
Istnieje również możliwość wysyłania zdarzenia do pojedynczych socketów, np. do tego, który aktualnie obsługujemy.
socket.on('message', (message) => {
console.log('Oh, I\'ve got something from ' + socket.id);
messages.push(message);
socket.emit('message', message)
});
Możemy to zrobić także dla wybranego, po jego id:
socket.on('message', (message) => {
console.log('Oh, I\'ve got something from ' + socket.id);
messages.push(message);
io.to('IIr5o6g-jQDfK3vcAAAC').emit('message', 'For your eyes only!');
});
Ostatecznie nasza funkcja będzie wyglądać tak:
socket.on('message', (message) => {
console.log('Oh, I\'ve got something from ' + socket.id);
messages.push(message);
socket.broadcast.emit('message', message);
});
Gotowe!
Nasza aplikacja czatu powinna od teraz działać już poprawnie.
Efekt jest zgodny z oczekiwaniami. Klient na bieżąco nasłuchuje na informacje z serwera. Kiedy dowie się o czymś nowym, doda to do swojej listy w HTML-u. W tym momencie sami też wyemitujemy serwerowi event. Z kolei serwer ciągle nasłuchuje na nowe wiadomości i jeśli takie dostanie, zapisuje je w tablicy messages, oraz przesyła dalej do następnych klientów. Oczywiście wszystko dzieje się w czasie rzeczywistym, bez żadnych opóźnień.
Jak widzisz, WebSocket nie jest zbyt wymagający w implementacji. Prawdopodobnie nawet użycie AJAX-u, który nie dałby nam aż tak dobrego efektu, wymagałoby od nas napisania większej ilości kodu.
Tylko od Ciebie zależy, w jak bardzo zaawansowany sposób wykorzystasz nowe umiejętności. WebSockety mogą przydać się w prostych aplikacjach czatu oraz w bardzo rozbudowanych grach z opcją multiplayer. Podstawowy koncept pozostanie jednak taki sam – komunikacja dwustronna serwer->klient, serwer jako pośrednik, oraz mechanizm event->nasłuchiwacz.
Jeśli czujesz, że to, co zrobiliśmy dotychczas, jest już dla Ciebie jasne, pozostaje tylko praktyka :)
Czas na trochę praktyki! Prawdopodobnie pomysł działania WebSocketów jest już dla Ciebie jasny, ale zapewne nie czujesz się jeszcze zbyt pewnie w tej materii. Dlatego mamy dla Ciebie dwa zadania. Oba będą polegały na rozwinięciu naszej aplikacji czatu. Powinny pozwolić Ci bardziej wgłębić się w temat.
Przechowywanie listy użytkowników – ten pomysł pojawił się już w materiale, choć nie wprowadzaliśmy go w życie. Teraz się to zmieni.
Twoim zadaniem jest dodanie do serwera tablicy users, która powinna zawsze posiadać aktualną listę użytkowników w formacie { name: 'John', id: '24423fse544w35' }. Oczywiście, atrybut id musi odpowiadać socket.id danego użytkownika.
Tablica powinna przechowywać tylko zalogowanych użytkowników, czyli takich, którzy wybrali już login. To ważna wskazówka. Oznacza bowiem, że nie dodamy naszej funkcjonalności pushowania do tablicy od razu po wykryciu connection. Zamiast tego, klient sam będzie musiał poinformować serwer o tym, że jest na to gotowy, kiedy już będzie wiedział, jaką nazwę wybrał użytkownik.
Usuwanie użytkownika z listy będzie łatwiejsze. Powinien znikać on od razu po zakończeniu komunikacji.
W celu testowania możesz dopisać kod, który po dodaniu nowego usera do tablicy, będzie informował o tym w konsoli. Robiliśmy to już z komunikatem Oh, I\'ve got something from... po dodaniu elementu do messages.
Pokaż wskazówkę nr 1 Ukryj wskazówkę nr 1
Do informowania serwera o wybraniu loginu, klient może wykorzystać nowy event np. o nazwie join.
Mogłoby to wyglądać następująco:
userName, klient emituje do serwera event join. Wraz z samym zdarzeniem wysyła oczywiście także login.join. Kiedy go wykryje, odbiera login i na bazie tej informacji oraz socket.id, dodaje nowy wpis do tablicy.Pokaż wskazówkę nr 2 Ukryj wskazówkę nr 2
Do usuwania użytkownika z listy możesz wykorzystać nasłuchiwacz na zdarzenie disconnect. Jeśli zostaje ono wykryte, to wiemy, że dany użytkownik wyłączył już naszą aplikację.
Korzystając z tego, co zrobisz w pierwszym zadaniu, spróbuj rozwinąć ten pomysł.
Twoim zadaniem jest dodanie do naszego czatu nowej funkcjonalności. Powinna ona działać następująco:
User-name has joined the conversation!, a jej autorem niech będzie Chat Bot. Byłoby dobrze, gdyby informacja ta odróżniała się od innych (np. tekst może być pochylony). Oczywiście w miejscu User-name powinna być nazwa dołączającego użytkownika.User-name has left the conversation... :(.Pokaż wskazówkę nr 1 Ukryj wskazówkę nr 1
users, teraz przesyłaj tę informację dalej, do pozostałych socketów (np. pod zdarzeniem o nazwie newUser albo join – Twój wybór). Pamiętasz jak wcześniej "rozsyłaliśmy" dalej zdarzenie message? Tutaj będzie to wyglądało prawie identycznie.Pokaż wskazówkę nr 2 Ukryj wskazówkę nr 2
Każdy klient powinien mieć ustawiony nasłuchiwacz na to zdarzenie, o którym mowa we wskazówce pierwszej (np. newUser). Kiedy je wykryje, powinien dodać nową wiadomość do listy (z wykorzystaniem addMessage). Jako parametr author wpisz Chat Bot, a jako content tekst User-name has joined the conversation!. Przy czym jako User-name przyjmij to, co otrzymasz wraz z eventem (a więc login).
Pokaż wskazówkę nr 3 Ukryj wskazówkę nr 3
Usuwanie użytkownika możesz rozegrać analogicznie. Gdy wykryjesz na serwerze event disconnect w danym sockecie, to wyemituj jakiś event do klientów (np. removeUser). Koniecznie wraz z tym eventem przekaż login użytkownika, który się rozłącza. Login ten możesz pobrać z tablicy users, wyszukując go, chociażby po id (socket.id). Gdy klient wykryje taki event, powinien analogicznie do dołączenia użytkownika, pokazywać wiadomość od Chat Bot, o treści User-name has left the conversation... :(. Znowu możesz skorzystać tutaj z funkcji addMessage.
Efekt powinien być następujący:
Wiemy już jak w teorii działają WebSockety, udało nam się również wykorzystać je w akcji, tworząc aplikację czatu. Jeden przykład może nie wystarczyć Ci, aby poczuć się wystarczająco pewnie, dlatego zrobimy kolejne zadanie.
Tym razem zabierzemy się za reactową aplikację listy zadań, czyli bardzo prosty przykład, w którym głównie chodzi o utrwalenie wiadomości. Z racji tego, że nie będzie tu żadnych nowości, zamiast zasypywać Cię snippetami, postaramy się pokazywać Ci tylko właściwą ścieżkę. Jeśli jednak czujesz się na siłach i naprowadzenie nie jest Ci potrzebne do szczęścia – przeczytaj tylko założenia i bierz się do pracy :)
Zamysł jest bardzo prosty.
['Shopping', 'Go out with a dog']).ul).li) powinien przedstawiać nie tylko treść zadania, ale oferować również button "Remove".Krótko mówiąc, chcemy mieć aplikację listy zadań, która może być współdzielona przez wielu użytkowników. Po wejściu powinniśmy otrzymywać aktualną listę zadań. Następnie, gdy którykolwiek z klientów coś w tej liście zmienia (czyli doda lub usunie), natychmiast powinni dowiadywać się o tym wszyscy pozostali klienci, tak aby mogli zaktualizować swoje lokalne tablice. Naturalnie pośrednikiem w przekazywaniu tych informacji i danych będzie serwer, a o "natychmiastowość" wszystkich operacji zadba Socket.IO.
Podsumujmy, jak może wyglądać serwer i klient.
Serwer powinien:
connection) i w sytuacji wykrycia go, podać temu klientowi aktualną listę zadań. Oczywiście będziemy je wysyłać wraz ze zdarzeniem (nazwijmy je updateData).removeTask) i w sytuacji wykrycia go, usunąć ten element również ze swojej tablicy zadań, a następnie jeszcze wyemitować zdarzenie dalej, do pozostałych klientów, tak aby mogli dokonać stosownej aktualizacji.addTask) i w sytuacji wykrycia go, dodać ten element do swojej tablicy zadań, oraz emitować to zdarzenie dalej do pozostałych klientów, tak aby mogli dokonać stosownej aktualizacji.Klient powinien:
Remove dla każdego z nich).updateData i w sytuacji wykrycia go, zaktualizować swoją tablicę zadań zgodnie z danymi otrzymanymi wraz z tym zdarzeniem od serwera.Remove usuwać dany element z tablicy lokalnej i emitować event removeTask do serwera, wraz z indeksem elementu do usunięcia.addTask do serwera wraz z nazwą zadania.removeTask i w sytuacji wykrycia go, usuwać element o otrzymanym id ze swojej tablicy.addTask i w sytuacji wykrycia go, dodawać nowy element o treści otrzymanej wraz ze zdarzeniem do swojej tablicy.Opis jest dość długi, ale pewnie widzisz, że funkcjonalności nie są przekombinowane, a sam sposób działania bardzo przypomina to, co robiliśmy już z czatem.
To jak? Może spróbujesz poradzić sobie całkowicie bez naszego udziału? ;)
Jeśli masz ochotę na wyzwanie, na tym etapie kończy się nasza pomoc. Jeśli jednak nie czujesz się jeszcze zbyt pewnie, poniżej znajdziesz dokładniejszą instrukcję.
Rozpocznij od założenia nowego katalogu projektu. Bezpośrednio w nim utwórz plik server.js (to będzie nasz serwer) oraz folder client (na naszą aplikację klienta).
Następnie, w katalogu głównym projektu utwórz plik package.json, pobierz Express, a na końcu zmodyfikuj server.js. Jego zadaniem ma być startowanie serwera pod portem 8000, a gdy się to uda, wypisywanie również w konsoli komunikatu Server is running.... Na ten moment serwer nie musi jeszcze obsługiwać endpointów. Opcjonalnie możesz ustawić jeden middleware, które będzie wyłapywał wszystkie requesty i zwracał komunikat Not found, nie jest to jednak obligatoryjne.
Pokaż wskazówkę Ukryj wskazówkę
const express = require('express');
const app = express();
const server = app.listen(process.env.PORT || 8000, () => {
console.log('Server is running...');
});
app.use((req, res) => {
res.status(404).send({ message: 'Not found...' });
});
Możesz uruchomić teraz komendę node server.js. Jeśli wszystko poszło dobrze, w konsoli powinien pojawić się komunikat Server is running....
Warto pobrać na tym etapie również Nodemona oraz dodać do package.json skrypt, który będzie z jego pomocą uruchamiał nasz server.js ("start": "nodemon server.js").
Następnie, z wykorzystaniem Create React App, wygeneruj w katalogu client reactowy szablon aplikacji.
Pokaż wskazówkę Ukryj wskazówkę
npx create-react-app client
Teraz usuń zbędne elementy z folderu client/src:
App.cssApp.test.jslogo.svgserviceWorker.jsUprość także plik index.js.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
Zrób porządek również w App.js.
import React from 'react';
class App extends React.Component {
render() {
return (
<div className="App">
Hello world
</div>
);
};
};
export default App;
Teraz uruchom w client komendę yarn start i sprawdź, czy podgląd naszej (na razie bardzo prostej) aplikacji pojawi się w przeglądarce.
Jeśli tak, to wszystko idzie zgodnie z planem. Mamy przygotowaną strukturę pod klienta oraz serwer.
Zacznijmy od stworzenia struktury HTML. Musimy przygotować listę, w której będziemy renderować zadania oraz formularz do ich dodawania.
Wejdź teraz do komponentu App i odpowiednio przygotuj funkcję render. Możesz wspomóc się poniższą ściągą.
return (
<div className="App">
<header>
<h1>ToDoList.app</h1>
</header>
<section className="tasks-section" id="tasks-section">
<h2>Tasks</h2>
<ul className="tasks-section__list" id="tasks-list">
<li class="task">Shopping <button class="btn btn--red">Remove</button></li>
<li class="task">Go out with a dog <button class="btn btn--red">Remove</button></li>
</ul>
<form id="add-task-form">
<input className="text-input" autocomplete="off" type="text" placeholder="Type your description" id="task-name" />
<button className="btn" type="submit">Add</button>
</form>
</section>
</div>
);
Jeśli chcesz, możesz wydzielić formularz oraz listę do osobnych komponentów, nie jest to jednak konieczne.
Aby aplikacja lepiej się prezentowała, dorzuć jeszcze trochę stylów do index.css:
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');
body {
background: #282c34;
color: #fff;
font-family: 'Open Sans', sans-serif;
padding: 30px;
text-align: center;
}
h1 {
font-weight: lighter;
}
p {
color: #999;
font-size: 0.8rem;
}
.btn {
background: #0064e7;
border: none;
border-radius: 15px;
padding: 10px 25px;
color: #fff;
font-family: inherit;
margin: 20px 10px;
text-transform: uppercase;
outline: none;
transition: .2s;
cursor: pointer;
}
.btn:hover {
box-shadow: 0 0 20px rgba(0,0,0,0.2);
}
.btn--red {
background: #e74c3c;
}
.text-input {
background: none;
border: none;
color: #fff;
border-bottom: 2px solid #0064e7;
padding: 8px 25px 8px 5px;
font-family: inherit;
outline: none;
opacity: 0.6;
transition: .3s;
}
.text-input:focus {
opacity: 1;
}
.tasks-section {
margin-bottom: 100px;
}
.tasks-section__list {
max-width: 400px;
list-style-type: none;
padding: 0;
margin: 20px auto 40px auto;
}
.task {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px dashed rgb(83, 83, 83);
}
W tej chwili, po uruchomieniu podglądu klienta, powinniśmy zobaczyć mniej więcej taki widok:
Podsumujmy, co na tym etapie udało nam się osiągnąć?
Mamy już działający serwer i cały widok klienta, choć brakuje jeszcze komunikacji między tymi dwoma podmiotami. W tej chwili kompletnie się ignorują i to nawet bardziej niż w aplikacji czatu. Tam też na początku nie było komunikacji, jednak serwer od zawsze zajmował się renderowaniem widoku klienta pod linkiem /. Tutaj nie mamy nawet tego, co więcej, nasz serwer oraz serwer podglądu aplikacji reactowej są uruchamiane na dwóch oddzielnych portach. To nic. Zaraz zmusimy je do współpracy!
Zajmijmy się odpowiednią modyfikacją serwera.
Zacznij od pobrania paczki Socket.IO i zaimportowania jej na serwerze, następnie zintegruj ją. Pamiętasz jak to zrobić? W razie problemu zajrzyj do przykładu z czatem.
Następnie utwórz tablicę tasks. Na starcie może być pusta, a później będziemy w niej przechowywać listę naszych zadań.
Potem, zgodnie z listą założeń, dodaj nasłuchiwacz na event connection. Przypomnijmy – to event, który jest wysyłany automatycznie przez każdy socket w momencie inicjacji połączenia (czyli np. gdy jakiś użytkownik otwiera naszą aplikację listy zadań). Jako callback dla tego nasłuchiwacza ustaw funkcję, której zadaniem będzie natychmiastowe emitowanie tylko do tego nowego użytkownika zdarzenia updateData. Wraz z samym zdarzeniem koniecznie wysyłana musi być też sama tablica tasks.
Pomysł jest prosty. Nowy użytkownik włącza aplikację, serwer od razu to wykrywa i wysyła do niego bieżącą lista zadań, żeby od początku miał u siebie aktualny widok.
Pamiętaj – chcesz wysyłać zdarzenie updateData tylko do jednego socketa, tego który właśnie zainicjował połączenie. Jeśli nie pamiętasz, jak możemy to osiągnąć, zajrzyj do poprzedniego submodułu.
Czas na kolejny krok. Chcemy, aby po wykryciu nowego klienta, serwer od razu zaczął nasłuchiwać też na dwa zdarzenia z jego strony.
Twoim zadaniem jest więc dodanie dwóch nasłuchiwaczy – na zdarzenie addTask oraz na removeTask.
W przypadku wykrycia pierwszego załóż że wraz ze zdarzeniem otrzymujesz od klienta również dane – nazwę taska. Właśnie takie zadanie (o nazwie otrzymanej wraz z eventem) dodaj do tablicy tasks, czyli jeśli np. otrzymujemy wraz ze zdarzeniem tekst Shopping, to do tablicy dodamy po prostu nowy element... Shopping. Dodatkowo, po tej operacji, serwer powinien wyemitować to samo zdarzenie (addTask), wraz z tą samą informacją, dalej do pozostałych socketów – wszystkich, z wyjątkiem tego, który dodał task.
W przypadku wykryciu drugiego eventu (removeTask), załóż, że to, co otrzymasz wraz ze zdarzeniem, jest indeksem elementu do usunięcia. Na bazie tej informacji serwer powinien usunąć ze swojej tablicy element o zgodnym indeksie. Po wszystkim powinien jeszcze wyemitować ten event wraz z informacją dalej, do pozostałych socketów. Oczywiście znowu z wyjątkiem tego, który usunął ten task.
Założenie obu nasłuchiwaczy jest tutaj proste. Chcemy, aby serwer miał zawsze aktualną listę zadań, oraz aby każdy klient był informowany o zmianach, a tablice ciągle aktualne.
Jeśli masz problem z niniejszym etapem, sprawdź, jak budowaliśmy nasz serwer w aplikacji z czatem. Rzut oka na kod z tamtego zadania (a dokładnie plik server.js) może Ci wiele wyjaśnić.
I... to tyle!
Zacznij od zaimportowania do pliku App.js paczki Socket.IO. W przykładzie z czatem nie mieliśmy Webpacka, więc nie było takiej możliwości. Musieliśmy ładować plik tej paczki w HTML-u przy użyciu tagu <script>. Teraz, skoro korzystamy już z Create React App, nic nie stoi na przeszkodzie, aby zaimportować tę paczkę bezpośrednio z node_modules. Create React App korzysta już bowiem z WebPacka.
Warto wiedzieć jednak, że nie musimy ładować całej funkcjonalności tej paczki. Możemy ograniczyć się do samego modułu odpowiedzialnego za możliwości klienta, bo tylko takie będą nam potrzebne.
import io from 'socket.io-client';
Następnie musisz zainicjować nasz socket. Możesz to zrobić w componentDidMount (a więc połączenie z serwerem wystartuje, gdy tylko komponent będzie gotowy).
W przykładzie z czatem klient i serwer byli dostępni jako jedna całość. Gdy włączaliśmy aplikację klienta, znajdowaliśmy się od razu na localhost:8000, a więc adresie, pod którym był nasz serwer. Dlatego też, przy inicjacji socketu (const socket = io();) nie musieliśmy podawać dokładnego adresu. Socket.IO domyślnie używa tego, na którym aktualnie się znajduje.
Teraz sytuacja jest inna. Serwer jest uruchamiany pod adresem localhost:8000, ale podgląd klienta już na localhost:3000. Socket.IO będzie starał się użyć tego, na którym aktualnie jesteśmy, a więc (localhost:3000). Koniecznie przy inicjacji socketu wskaż więc właściwy adres serwera localhost:8000.
W przykładzie z czatem przypisywaliśmy połączenie do stałej socket. Teraz nie byłoby to najlepszym wyjściem, bo stała zainicjowana w bloku jednej funkcji (componentDidMount) nie byłaby już dostępna w innych metodach. Lepiej więc przypisać połączenie do this.socket, a więc jako atrybut dostępny w każdej metodzie naszego komponentu.
Teraz przejdźmy już do realizacji założeń.
Zacznij od utworzenia tablicy zadań. Możesz nazwać ją tasks. Na starcie powinna być pusta.
Następnie przejdź do metody render. Odnajdź element #tasks-list i zmodyfikuj go w taki sposób, aby zamiast stałych elementów, li w liście były generowane na podstawie tablicy state.tasks. Możesz użyć metody map. Pamiętaj, aby w każdym renderowanym li, oprócz tekstu, umieścić również button "Remove".
Gdy lista będzie renderować się już poprawnie, zajmij się samym buttonem "Remove". Dodaj do niego nasłuchiwacz na kliknięcie (onClick). W przypadku wykrycia tego eventu powinna uruchamiać się funkcja (nazwijmy ją removeTask), która w założeniu będzie usuwać dany element z tablicy state.tasks. Przy przypinaniu tej funkcji do nasłuchiwacza pamiętaj też o przekazaniu do niej indeksu zadania – w końcu removeTask będzie musiała wiedzieć, który element z listy ma usunąć.
Czas na samą metodę removeTask. Powinna ona przyjmować jeden parametr (nazwijmy go id). Jej zadaniem jest usunięcie elementu z tablicy o indeksie, którego wartość jest równa właśnie id. To jednak jeszcze nie wszystko. Na razie usuwamy dane zadanie tylko lokalnie, a powinniśmy jeszcze poinformować o tym serwer, emitując zdarzenie. Pamiętaj o wysłaniu również id, aby serwer wiedział jaki element w swojej tablicy ma usunąć.
Czas na obsługę formularza do dodawania zadania.
Zacznij od stworzenia nowego atrybutu do stanu. Możesz nazwać go taskName. Następnie zmień pole w formularzu, aby jego wartość była pobierana właśnie ze state.taskName. Dodatkowo tak zmodyfikuj kod aplikacji, aby przy zmianie wartości w inpucie (onChange), automatycznie aktualizowała się też wartość zapisana w stanie (w state.taskName). Chodzi po prostu o to, aby była tam zawsze ta sama treść, która aktualnie jest wpisana również do pola tekstowego. Do samego formularza przypnij też funkcję na event submit. Gdy formularz wykryje to zdarzeniem, niech uruchamia nową metodę – submitForm.
Czas na jej napisanie. submitForm powinna mieć dostęp do obiektu zdarzenia i na samym starcie blokować domyślne zachowanie formularza. Następnie powinna uruchamiać metodę addTask (zaraz ją dodamy). Przy wywołaniu tej metody pierwszy argument powinien mieć wartość state.taskName, aby funkcja addTask wiedziała, jaka ma być treść nowego zadania.
Dlaczego nie mogliśmy od razu zaktualizować stanu? Zauważ, że potrzebujemy funkcji, która przyda się teraz, gdy dodajemy task lokalnie, ale i wtedy, kiedy otrzymamy informację o nowym zadaniu od serwera. Gdyby taką rolę pełnił submitForm, mogłoby się to nie sprawdzić. Zakładamy, że serwer zwróci nam od razu treść nowego tasku, jaki sens mogłoby mieć wtedy np. zatrzymywanie domyślnego zachowania formularza, które jest uruchamiane na starcie submitForm? W takiej sytuacji żaden. W końcu nawet nie obsługiwalibyśmy wtedy formularza tylko dostawalibyśmy od razu treść zadania wraz z nim. Dlatego też pójdziemy w innym kierunku. Stworzymy zaraz nową funkcję addTask, której zadaniem będzie tylko przyjmowanie w formie argumentu stringu z treścią zadania. Nie interesuje jej, czy pochodzi on ze state.taskName, czy może z informacji otrzymanej od serwera. Taka funkcja będzie mogła być używana od razu po wykryciu nowego tasku na serwerze, ale też właśnie przez naszą metodę submitForm.
Wróćmy jeszcze do submitForm. Na razie zatrzymuje ona domyślne zachowanie formularza oraz wywołuje addTask, która ma dodać nasze zadanie do lokalnej tablicy. Czy czegoś tu jeszcze brakuje? W sytuacji dodania przez nas lokalnie tasku, warto by informować o tym również serwer, aby mógł zatkualizować swoją tablicę, ale też podać informację o potrzebie aktualizacji innym klientom. Po wywołaniu metody addTask dodaj więc jeszcze kod, który zajmie się emitowaniem do serwera zdarzenia addTask. Wraz z tym zdarzeniem musi być oczywiście wysyłana sama treść nowego tasku, a więc state.taskName.
Teraz już czas na samą metodę addTask. Powinna przyjmować jeden parametr (nazwijmy go task) i to z niego otrzymamy nowe zadanie do state.tasks.
Od tego momentu dodawanie i usuwanie elementów powinno już w Twojej lokalnej aplikacji działać. Pozostaje nam tylko jedna rzecz – dodanie nasłuchiwaczy na zdarzenia, które może do nas emitować serwer. Chcemy też, aby każdy klient był w stanie umiejętnie reagować na zmiany wprowadzane przez innych użytkowników, o których serwer zawsze będzie nas informował.
Musimy dodać trzy nasłuchiwacze. Pierwszy na zdarzenie addTask, drugi na removeTask oraz trzeci na updateData, czyli event, który jest wysyłany jako swego rodzaju powitanie od serwera. Gdy dodasz je w componentDidMount, wtedy będą nasłuchiwać na wszystkie zdarzenia od momentu, gdy tylko komponent App będzie gotowy.
Kiedy klient wykryje addTask, powinien odebrać dane przekazywane samym eventem, a następnie wywołać lokalną metodę addTask. Jako argument przy wywołaniu tej metody należy przekazać informację otrzymaną od serwera (zakładamy, że serwer podał nam treść zadania dodanego przez innego klienta). Zapewni to nas, że gdy tylko jakiś inny użytkownik dołoży nowe zadanie, to my, poinformowani o tym fakcie przez serwer, też dodamy je do swojej lokalnej tablicy.
Kiedy wykryjemy removeTask, powinniśmy odebrać to, co jest przekazywane do nas wraz z samym eventem (indeks elementu do usunięcia) i wywoływać lokalną metodę removeTask. Jako argument przy wywołaniu należy przekazać indeks, by metoda wiedziała, co właściwie ma usunąć. Dzięki temu, gdy tylko jakiś inny użytkownik skasuje któreś zadanie, to my poinformowani o tym fakcie przez serwer, też pozbędziemy się go ze swojej lokalnej tablicy.
Musisz bez naszej pomocy rozwiązać jeszcze jeden problem. W tej chwili nieważne czy otrzymujemy dane o zadaniu do usunięcia od serwera, czy też sami je usuwamy, uruchamiamy dokładnie tę samą funkcję – removeTask. Nie jest to do końca idealne rozwiązanie. Zobacz, że funkcja ta nie tylko usuwa task z tablicy, ale również emituje informacje o potrzebie usunięcia tego tasku do serwera. Jest to w porządku do momentu, kiedy faktycznie to my usuwamy task i chcemy, aby serwer podał informację o tym do innych klientów.
Wyobraź sobie jednak, że to nie my, lecz inny klient wykonał tę operację. Co uruchomi się u nas w takiej sytuacji? Wiemy, że klient wykryje u nas zdarzenie removeTask i włączy lokalną funkcję removeTask, a ona, zgodnie z informacją od serwera, usunie poprawnie zadanie, ale też... sama wyemituje taką akcję do serwera. Serwer wykryje event i po raz kolejny przekaże informację o potrzebie usunięcia tego konkretnego tasku do pozostałych klientów. Te będą chciały usunąć je u siebie i znowu poinformują serwer o potrzebie usunięcia tej konkretnej wiadomości. Czyli... event będzie krążył w kółko i nigdy nie przestanie.
Musisz więc przemodelować tę funkcję, aby emitowanie zdarzenia do serwera miało miejsce tylko wtedy, gdy akcja usunięcia była wykonana najpierw lokalnie przez dany socket. Jeśli ta funkcja odpali się po informacji o potrzebie aktualizacji od serwera, powinna tylko usunąć odpowiedni element w swojej lokalnej tablicy. Krótko mówiąc, musisz uwarunkować odpowiednio emitowanie zdarzenia removeTask przez metodę removeTask.
Co do eventu updateData, to serwer przesyła wraz z nim informację o aktualnej tablicy z taskami. Zdarzenie jest emitowane przez serwer do klienta raz, kiedy ten inicjuje połączenie. Chcemy bowiem, aby użytkownik po włączeniu aplikacji zobaczył, jaka jest aktualna tablica zadań, inaczej niż w aplikacji czatu, gdzie archiwalne wiadomości nie były dla nas ważne. Potem już jednak o każdej pojedynczej zmianie klient zostanie informowany na bieżąco (poprzez eventy addTask i removeTask) oraz możliwe stanie się dodawanie i usuwanie elementów. Serwer nie będzie musiał już przesyłać do niego całej tablicy, tylko informować, że trzeba dodać albo usunąć jeden element.
Wykrycie tego eventu powinno powodować wywołanie nowej funkcji (np. updateTasks), której zadaniem będzie przypisanie otrzymanych wraz eventem danych do state.tasks. Tak naprawdę, klient będzie tutaj otrzymywał całą tablicę z taskami, które były dodawane do momentu jego dołączenia i zapisze ją jako swoją lokalną state.tasks. Oczywiście napisanie tej metody (updateTasks) to też Twoje zadanie.
Na razie to tyle!
Klient pozwala od teraz na dodawanie i usuwanie elementów z tablicy. O każdej zmianie natychmiastowo informowany jest serwer, który o potrzebie aktualizacji powiadamia pozostałych klientów. Naturalnie klienci muszą na takie informacje odpowiednio nasłuchiwać, dlatego też dodaliśmy nasłuchiwacze, które powodują, że wykrycie informacji o zmianach w tablicy na którymś z socketów jest równoznaczne z natychmiastową aktualizacją lokalnych danych w kliencie.
Pozostaje już tylko przetestować, czy wszystko działa poprawnie. Jeśli udało Ci się dobrze wykonać wszystkie instrukcje, to efekt powinien być następujący:
Jak na razie, struktura danych w naszej tablicy zadań jest bardzo prosta. Tak naprawdę to tylko lista stringów, co powoduje, że identyfikacja elementów jest możliwa jedynie po ich indeksie (w końcu treść zadań może być powtarzalna, nie jest unikalna). Właśnie z takiej opcji korzystaliśmy np. przy ustalaniu, które zadanie ma być usunięte przez metodę removeTask. Jak zapewne się domyślasz, nie jest to najbardziej profesjonalna opcja. Indeksy nadawane są po kolei, a więc usunięcie jednego elementu powoduje, że wszystkie zadania, które miały indeks większy od tego elementu, zwyczajnie otrzymują nagle nowy indeks, mniejszy o jeden.
Twoim zadaniem jest więc takie zmodyfikowanie aplikacji, aby dane były przechowywane w bardziej skomplikowanym formacie. Każdy task powinien być zapisywany w tablicy jako obiekt z dwoma atrybutami – pierwszy (id) powinien przechowywać unikalny identyfikator, a drugi (name) już samą treść zadania. Zatem dane w tablicy będą wyglądały po zmianach np. tak:
[{ id: 'dfsadf324s', name: 'Shopping'}, { id: 'dfs2ad6724s', name: 'Go out with a dog'}];
Sam identyfikator powinien być nadawany podczas dodawania tasku lokalnie, zanim klient wyśle informację o nim serwerowi. Do wygenerowania unikalnego identyfikatora możesz użyć zewnętrznej paczki (np. uuid).
Oczywiście task będzie wymagał od Ciebie również innych modyfikacji. Przed wykonaniem zadania przejrzyj cały kod serwera oraz klienta i dobrze zastanów się, gdzie będą potrzebne zmiany.
Jeśli to dla Ciebie za mało, spróbuj jeszcze zaimplementować do aplikacji funkcjonalność edycji zadań. Podejmiesz rękawicę? ;)
Czas na kolejne zadanie praktyczne. Tym razem będzie krótsze, jednak nieco różniące się od przykładów z czatem i listą zadań.
Do tej pory, gdy używaliśmy WebSocketów, stawały się one od razu najważniejszym elementem aplikacji, a także jedynym środkiem komunikacji pomiędzy serwerem a klientem. Nie zawsze musi tak być. W tym zadaniu dowiesz się, jak łatwo możemy wykorzystywać WebSockety tylko w części aplikacji oraz jak implementować tę technologię obok AJAX-u, a nie zamiast niego.
Będziemy pracować nad witryną festiwalu muzycznego z poprzedniego modułu, a konkretnie – zajmiemy się modyfikacją podstrony do rezerwacji biletów. Mówiliśmy o tym, jak niewydajny jest nasz obecny pomysł na sprawdzanie ilości wolnych miejsc i aby go poprawić, zastosujemy WebSockety.
Zacznijmy od przeanalizowania tego, jak aktualnie działa nasza aplikacja i jak ma się to zmienić.
W tej chwili informacja o zajętości jest pobierana automatycznie, kiedy użytkownik wchodzi na podstronę "Order a ticket", albo kiedy zarezerwuje miejsce. Oprócz tego jest również odświeżana co dwie minuty. Wiemy jednak, że bardzo często te "cykliczne" żądania są całkowicie zbędne. W końcu może dojść do takiej sytuacji, gdzie przez godzinę nikt nie kupi ani jednego biletu, a klient i tak wykona w tym czasie multum niepotrzebnych requestów.
Teraz zabierzemy się do tego z wykorzystaniem WebSocketów, a więc z użyciem emitterów i nasłuchiwaczy.
Zastanów się chwilę nad tym, jak by to mogło dokładnie wyglądać, a następnie przeczytaj nasz pomysł poniżej i skonfrontuj oba scenariusze.
Nasza propozycja jest następująca. Zamiast wysyłać żądania cyklicznie, co dwie minuty, klient będzie ciągle nasłuchiwał na jedno zdarzenie z serwera. Nazwijmy je seatsUpdated. Kiedy je wykryje, to za pomocą danych, które otrzyma wraz ze zdarzeniem, odświeży podgląd "zajętości" miejsc. Sam serwer będzie emitował to zdarzenie do wszystkich klientów, gdy tylko w bazie danych pojawi się nowa rezerwacja. Dzięki temu korzystanie z podstrony stanie się o wiele wygodniejsze – nie będziemy co chwilę spoglądać na "loader" informujący o kolejnym połączeniu, a klient zaprzestanie wysyłania zbędnych żądań. Serwer sam poinformuje nas o potrzebie aktualizacji, jeśli ktoś faktycznie zarezerwuje bilet. Wiedząc, że WebSockety działają w czasie rzeczywistym, mamy pewność, że informacja faktycznie jest adekwatna do tego, co znajduje na serwerze, a nie wskazuje stan "sprzed dwóch minut".
A co z samym wysyłaniem rezerwacji? Wciąż możemy korzystać z obecnego rozwiązania. Nie ma potrzeby używania WebSocketów do wysyłki formularza. Oczywiście moglibyśmy to zrobić – wystarczyłoby utworzyć odpowiedni nasłuchiwacz na serwerze, a klientowi kazać emitować zdarzenie po kliknięciu na "Submit". Tylko czy to naprawdę miałoby sens? Musielibyśmy przemodelować kod, nie otrzymując tak naprawdę w zamian żadnej wymiernej korzyści. Nie potrzebujemy tutaj bowiem ani ciągłej komunikacji, ani możliwości inicjacji czegoś przez serwer. Nie zapominaj, że AJAX domyślnie opiera się na mechanizmie request->response, jest wręcz do niego stworzony, przez co pozwala nam na przekazanie ewentualnych odpowiedzi z informacją o błędzie albo o sukcesie. W przypadku WebSocketów musielibyśmy po całej operacji dodania rezerwacji na serwerze, jeszcze informować o efekcie poprzez emitowanie nowego zdarzenia. Tak naprawdę kod by się wydłużył, więc akurat w tej sytuacji, AJAX będzie zdecydowanie lepszym wyborem.
Wiemy już, co chcemy osiągnąć i jak to zrobić, lecz zaczniemy od podstawowej konfiguracji. Nie będziemy jeszcze niczego emitować, tylko przygotujemy "obserwacje" na dołączenie nowego socketu (klienta) i będziemy informować o tym w konsoli. Na razie – zero nowości.
Zacznij od znalezienia najnowszej wersji naszej witryny festiwalu, było to ostatnie zadanie w poprzednim module.
Na samym początku pobierz paczkę Socket.IO i zaimportuj ją w Twoim pliku server.js.
W kolejnym kroku zintegruj możliwości tej paczki z naszym serwerem.
Następnie przygotuj nasłuchiwacz na event, który jest domyślnie i automatycznie wysyłany przez klienta przy dołączeniu (connection). Jeśli serwer go wykryje, niech informuje o tym w konsoli za pomocą komunikatu New socket!. To na razie wszystko.
Na tym etapie serwer powinien mieć dostęp do funkcjonalności Socket.IO oraz oczekiwać na dołączenie socketu. Jeśli je wykryje, wypisze w konsoli odpowiedni komunikat.
Aby sprawdzić, czy serwer dobrze reaguje na sockety, musimy zmodyfikować samego klienta. W tym miejscu pojawia się pytanie. Czy socketem będzie cała aplikacja? Albo inaczej – czy socketem musi być cała aplikacja? W końcu tak wyglądało to w naszym ostatnim przykładzie z listą zadań. Odpowiedź brzmi – nie. Tak naprawdę, zakładając, że mechanizm WebSocketów jest nam potrzebny tylko na jednej podstronie, nie ma sensu inicjować go zawsze. Zastanów się tylko. Gdybyśmy inicjowali socket w kliencie od razu, np. w komponencie App, to każde wejście na stronę otwierałoby ciągłą i dwustronną komunikację z serwerem, nawet wtedy, gdy użytkownik nie odwiedziłby podstrony rezerwacji biletu. Czy powodowałoby to jakiś błąd? Nie, ale niepotrzebnie wymagałoby od serwera i klienta utrzymywania stałej komunikacji. O wiele lepiej będzie zainicjować socket w kliencie, wtedy kiedy faktycznie potrzebujemy jego możliwości, a więc np. w naszym komponencie SeatChooser. W takim przypadku komunikacja klienta z serwerem będzie startowała tylko wtedy, kiedy użytkownik wejdzie na podstronę rezerwacji biletu.
Otwórz teraz właśnie ten komponent. Na razie nie będziemy jeszcze tworzyć tutaj szczegółowej logiki, chcemy tylko zainicjować socket i sprawdzić, czy faktycznie wejście w naszą aplikację, a następnie skierowanie się do podstrony rezerwacji, będzie odpowiednio zauważone przez serwer.
Zacznij od zaimportowania paczki Socket.IO, nie musi być cała, wystarczy część kliencka.
Następnie w odpowiednim miejscu zainicjuj nasze połączenie z serwerem. Referencje do niego przechowuj w this.socket.
Pamiętaj, że nasz klient może być na innym adresie niż serwer (localhost:3000 – w wersji developerskiej), a czasem na takim samym (localhost:8000 lub serwer Heroku – aplikacja po zbudowaniu). Koniecznie warunkuj połączenie od process.env.NODE_ENV. Jeśli jesteśmy w środowisku production, to niech Socket.IO automatycznie wybierze domyślny adres, w innej sytuacji wskaż jednak localhost:8000.
Teraz uruchom serwer oraz klienta i sprawdź, czy odpalenie podglądu aplikacji, a następnie wejście na podstronę "Order a ticker", zostało odnotowane w konsoli komunikatem New socket!. Jeśli się pojawił, wszystko jest na razie w porządku.
Skoro wiemy już, że połączenie jest w porządku, czas przejść do realizacji naszego planu. Zacznijmy od serwera.
Chcemy, aby po dodaniu rezerwacji do bazy danych, rozgłosił on wszystkim socketom zdarzenie mówiące o potrzebie aktualizacji. Oczywiście dobrze byłoby wysyłać również aktualną kolekcję z rezerwacjami.
Musisz odnaleźć teraz endpoint, który jest uruchamiany przy żądaniu dodania nowej rezerwacji (/seats, metoda POST). W tej chwili przypięta do niego funkcja jedynie waliduje dane, dodaje nową rezerwację do tablicy, a następnie zwraca komunikat OK. Twoim zadaniem jest taka modyfikacja tej funkcji, aby bezpośrednio po dodaniu rezerwacji do tablicy, serwer wyemitował także odpowiednie zdarzenie do wszystkich klientów. Nazwij je seatsUpdated (zgodnie z wcześniejszymi założeniami). Wraz z samym zdarzeniem wysyłaj też aktualną zawartość kolekcji (db.seats).
Pojawia się tylko jeden problem. Stała io, która jest referencją do naszego serwera i z której moglibyśmy wyemitować zdarzenie, jest obecna w pliku server.js, a nasz endpoint w innym. Nie mamy do tej stałej dostępu. Co możemy z tym zrobić?
W poprzednim module bardzo często z opresji ratowało nas użycie middleware. Tym razem będzie podobnie.
Pamiętasz jak na początku poprzedniego modułu tworzyliśmy następujący middleware?
app.use((req, res, next) => {
res.show = (name) => {
res.sendFile(path.join(__dirname, `/views/${name}`));
};
next();
});
Jego rolą było dodanie do obiektu res nowej metody, która mogłaby być potem używana w każdym z endpointów. Tego właśnie szukamy. Moglibyśmy w taki sposób w server.js dodać middleware, który będzie uruchamiany przed konkretnymi endpointami (a więc również przed /seats POST) i doda do obiektu req lub res dodatkowy parametr, który będzie referencją do naszego io, na przykład req.io. Dzięki temu mielibyśmy do niego dostęp w każdym endpoincie, nawet w takim, który jest w zewnętrznym pliku.
Dopisz więc do pliku server.js nowy middleware. Pamiętaj, aby koniecznie wstawić go przed middleware dodającym endpointy z zewnętrznych plików.
app.use((req, res, next) => {
req.io = io;
next();
});
Teraz w każdym pliku, również zewnętrznym, endpointy będą miały dostęp do naszego serwera WebSocket przy użyciu zwykłego obiektu żądania req (a dokładnie req.io).
Możemy wrócić w tej chwili do naszego endpointu. Skoro masz już dostęp do obiektu io, nie pozostaje Ci nic innego jak dodać w nim kod, który będzie emitował odpowiednie zdarzenie do wszystkich klientów, gdy tylko serwer doda już samą rezerwację do kolekcji w tablicy.
I... to wszystko.
Czas na klienta. Wiemy już, że serwer, gdy tylko doda nową rezerwację do kolekcji w bazie danych (a raczej tablicy, która taką bazę udaje), od razu wyemituje zdarzenie seatsUpdated do wszystkich klientów. Dzięki temu będą zdawali sobie sprawę, że "zajętość" się zmieniła i trzeba zaktualizować widok miejsc. Oczywiście ustaliliśmy, że serwer z samym zdarzeniem będzie wysyłać również aktualną tablicę rezerwacji. Teraz jednak musimy jeszcze dodać odpowiedni nasłuchiwacz w samym kliencie, tak aby wykrywał ten event i coś w takiej sytuacji robił. Co dokładnie? Najlepiej odświeżał po prostu widok miejsc zgodnie z tym, co dostanie od serwera.
Zacznij od dodania nasłuchiwacza na event seatsUpdated. Przygotuj się też na to, że wraz ze zdarzeniem serwer wysyła tablicę aktualnych rezerwacji – odbieraj je jako argument seats. Następnie zastanówmy się jaką funkcję powinniśmy uruchamiać, gdy wykryjemy ten event. Na razie w naszym komponencie mamy metodę loadSeats, wiemy przy tym, że jest ona odwołaniem do thunka loadSeatsRequest. Ona raczej się nie nada, bo przecież jej podstawowe działanie to... połączenie AJAX-owe do serwera. Po co nam ono, skoro my otrzymujemy już wszystkie potrzebne dane z serwera wraz ze zdarzeniem? Zamiast tego, lepszym pomysłem będzie przygotowanie w containerze funkcji z bezpośrednim wywołaniem dispatchu akcji loadSeats z Reduksa, a następnie wykorzystanie jej w przypadku wykrycia eventu seatsUpdated. Oczywiście jako payload będziemy tutaj przekazywali dane otrzymane od serwera.
Pokaż wskazówkę Ukryj wskazówkę
SeatChooserContainer.js i zaimportuj z ../../../redux/seatsRedux potrzebny kreator akcji (loadSeats).mapDispatchToProps dodaj nowy parametr, np. loadSeatsData. Powinien on być nową funkcją strzałkową, która przyjmuje parametr seats, a następnie uruchamia dispatch(loadSeats(seats)). Od tej chwili w komponencie SeatChooser mamy już dostęp do funkcji, która będzie potrafiła zmodyfikować bezpośrednio nasz store reduksowy.seatsUpdated. Uruchamiaj ją, a jako argument przekazuj dane otrzymane od serwera wraz ze zdarzeniem.W takim układzie, możemy zrezygnować w końcu z cyklicznego odświeżania liczby zajętych miejsc. Mamy już pewność, że w razie jakiejś zmiany, serwer nas o tym poinformuje, a klient będzie wiedział, co z taką informacją powinien zrobić. Podobnie nie musimy już uruchamiać loadSeats po dodaniu samej rezerwacji. Odnajdź więc funkcję submitForm w komponencie OrderTicketForm i usuń jej wywoływanie. Możesz też usunąć odpowiednią referencję z containera.
To tyle :)
Końcowy efekt działania Twojej aplikacji powinien być następujący:
Tym razem musieliśmy napisać o wiele mniej kodu, ale cel tego zadania był inny niż wcześniej. Po prostu chcieliśmy uświadomić Ci, że WebSockety nie zawsze muszą obsługiwać całą komunikację pomiędzy serwerem a klientem.
Twoim zadaniem będzie rozwinięcie komponentu SeatChooser tak, aby pod widokiem krzesełek widniało również podsumowanie "zajętości" – wystarczy zwykły akapit w formacie Free seats: 2/15. Oczywiście liczba powinna wskazywać, ile jest wolnych miejsc na wszystkie możliwe, musi też być natychmiastowo aktualizowana w przypadku rezerwacji przez któregokolwiek z użytkowników.
Możesz stworzyć nowe zdarzenia, dodać nasłuchiwacze, a może... już masz wszystkie potrzebne informacje i wystarczy je odpowiednio wykorzystać?
Gdy zadanie będzie gotowe, zapisz zmiany w Gicie, załaduj je na Heroku, a link do opublikowanej witryny wyślij mentorowi.
Co musisz zapamiętać z tego modułu? Przede wszystkim, czym jest technologia WebSocketów – jak działa, do czego służy, kiedy warto z niej skorzystać. Na tym etapie tajemnicą nie powinien być dla Ciebie również mechanizm emitowania i nasłuchiwania.
Należy tu przypomnieć jeszcze jeden ważny wniosek. WebSockety to nie konkurencja dla AJAX-u ani żadna alternatywa. Obie technologie działają w różny sposób, mają swoje zastosowania i zadania, w których się sprawdzają. Jak zawsze, to developer musi ocenić, który wybór w danej sytuacji będzie właściwy.
Na koniec tego modułu przygotowaliśmy dla Ciebie quiz powtórkowy. Pomoże Ci on powtórzyć wiedzę z poprzednich modułów.
Odpowiedzi tego quizu nie są nigdzie zapisywane, więc są tylko do Twojej wiadomości. Ten quiz ma Ci posłużyć jako pomoc w nauce – dlatego pod każdym pytaniem znajdziesz guzik, który sprawdzi poprawność Twoich odpowiedzi oraz poda Ci wyjaśnienie zagadnienia poruszanego w tym pytaniu.
1. Do czego w Expressie służy metoda res.send(arg)?
Tej metody używamy głównie w celu wyświetlenia komunikatów o błędzie.
2. Do czego w Expressie służy metoda express.static(arg)?
Dzięki tej metodzie możemy zdefiniować katalog zawierający pliki, które mają być udostępniane przez nas serwer.
3. Do czego w Expressie służy metoda res.show(arg)?
Chcąc wysyłać do przeglądarki zawartość pliku HTML, użyjemy właśnie tej metody.
4. Do czego w Expressie służy metoda res.render(arg)?
Jeśli używamy widoków opartych o Handlebars, użyjemy tej metody do wyrenderowania kodu HTML na podstawie konkretnego szablonu. Zwykle będziemy dodawać drugi argument, zawierający dane, z których korzysta ten szablon.
5. Do czego w Expressie służy metoda res.status(arg)?
W komunikacji HTTP ważne jest ustawianie statusów, szczególnie kiedy wystąpiła nietypowa sytuacja – np. nie znaleziono żądanego pliku, adres żądanej strony został zmieniony i chcemy przekierować klienta pod nowy adres, użytkownik musi się zalogować, aby uzyskać dostęp do żądanego adresu, etc.